Index: Doc/library/os.rst =================================================================== --- Doc/library/os.rst (revision 75723) +++ Doc/library/os.rst (working copy) @@ -830,7 +830,7 @@ Like :func:`stat`, but do not follow symbolic links. This is an alias for :func:`stat` on platforms that do not support symbolic links, such as - Windows. + Windows prior to 6.0 (Vista). .. function:: mkfifo(path[, mode]) @@ -1094,10 +1094,26 @@ .. function:: symlink(source, link_name) - Create a symbolic link pointing to *source* named *link_name*. Availability: - Unix. + Create a symbolic link pointing to *source* named *link_name*. On Windows, + symlink version takes an additional, optional parameter, target_is_directory, + which defaults to False + + symlink(source, link_name, target_is_directory=False) + On Windows, a symlink represents a file or a directory, and does not + morph to the target dynamically. For this reason, when creating a + symlink on Windows, if the target is not already present, the symlink + will default to being a file symlink. If target_is_directory is set to + True, the symlink will be created as a directory symlink. This + parameter is ignored if the target exists (and the symlink is created + with the same type as the target). + Symbolic link support was introduced in Windows 6.0. *symlink* will raise + a NotImplementedError on Windows versions earlier than 6.0. + + Availability: Unix, Windows. + + .. function:: unlink(path) Remove (delete) the file *path*. This is the same function as Index: Lib/ntpath.py =================================================================== --- Lib/ntpath.py (revision 75723) +++ Lib/ntpath.py (working copy) @@ -306,17 +306,29 @@ return split(p)[0] # Is a path a symbolic link? -# This will always return false on systems where posix.lstat doesn't exist. +# This will always return false on systems where os.lstat doesn't exist. def islink(path): - """Test for symbolic link. - On WindowsNT/95 and OS/2 always returns false + """Test whether a path is a symbolic link. + This will always return false for Windows prior to 6.0 + and for OS/2. """ - return False + try: + st = os.lstat(path) + except (os.error, AttributeError): + return False + return stat.S_ISLNK(st.st_mode) -# alias exists to lexists -lexists = exists +# Being true for dangling symbolic links is also useful. +def lexists(path): + """Test whether a path exists. Returns True for broken symbolic links""" + try: + st = os.lstat(path) + except (os.error, WindowsError): + return False + return True + # Is a path a mount point? Either a root (with or without drive letter) # or an UNC path with at most a / or \ after the mount point. Index: Lib/test/test_win_symlink.py =================================================================== --- Lib/test/test_win_symlink.py (revision 0) +++ Lib/test/test_win_symlink.py (revision 0) @@ -0,0 +1,59 @@ +import os +import sys +import stat + +import unittest + +class WindowsSymlinkTest(unittest.TestCase): + filelink = r'filelinktest' + filelink_target = os.path.abspath(__file__) + dirlink = r'dirlinktest' + dirlink_target = os.path.dirname(filelink_target) + + def setUp(self): + if (not hasattr(sys, 'getwindowsversion') or + sys.getwindowsversion() < (6,) + ): raise Exception("This test requires Windows Vista or greater") + + assert os.path.exists(self.dirlink_target) + assert os.path.exists(self.filelink_target) + assert not os.path.exists(self.dirlink) + assert not os.path.exists(self.filelink) + + def test_directory_link(self): + os.symlink(self.dirlink_target, self.dirlink) + self.assertTrue(os.path.exists(self.dirlink)) + self.assertTrue(os.path.isdir(self.dirlink)) + self.assertTrue(os.path.islink(self.dirlink)) + self.check_stat(self.dirlink, self.dirlink_target) + + def test_file_link(self): + os.symlink(self.filelink_target, self.filelink) + self.assertTrue(os.path.exists(self.filelink)) + self.assertTrue(os.path.isfile(self.filelink)) + self.assertTrue(os.path.islink(self.filelink)) + self.check_stat(self.filelink, self.filelink_target) + + def test_remove_directory_link_to_missing_target(self): + linkname = "missing link" + target = r'c:\\target does not exist.29r3c740' + target_is_dir = True + os.symlink(target, linkname, target_is_dir) + # self.assertTrue(os.path.isdir(linkname)) # fails + # normally, use rmdir to remove directory links + #os.rmdir(linkname) + # For compatibility with Unix, os.remove will check the + # directory status and call RemoveDirectory if the symlink + # was created with target_is_dir==True. + os.remove(linkname) + + def check_stat(self, link, target): + self.assertEqual(os.stat(link), os.stat(target)) + self.assertNotEqual(os.lstat(link), os.stat(link)) + + def tearDown(self): + if os.path.exists(self.filelink): os.remove(self.filelink) + if os.path.exists(self.dirlink): os.rmdir(self.dirlink) + +if __name__ == "__main__": + unittest.main() Property changes on: Lib\test\test_win_symlink.py ___________________________________________________________________ Added: svn:keywords + Id Rev Author Date Index: Modules/posixmodule.c =================================================================== --- Modules/posixmodule.c (revision 75723) +++ Modules/posixmodule.c (working copy) @@ -618,7 +618,7 @@ #ifdef MS_WINDOWS static PyObject * -win32_error(char* function, char* filename) +win32_error(char* function, const char* filename) { /* XXX We should pass the function name along in the future. (winreg.c also wants to pass the function name.) @@ -1024,12 +1024,31 @@ return TRUE; } +/* +About the following functions: win32_lstat, win32_lstat_w, + win32_stat, win32_stat_w + + In Posix, stat automatically traverses symlinks and returns + the stat structure for the target. In Windows, the equivalent + GetFileAttributes by default does not traverse symlinks and + instead returns attributes for the symlink. + + Therefore, win32_lstat will get the attributes traditionally, + and win32_stat will first explicitly resolve the symlink target + and then will call win32_lstat on that result. + + The _w represent Unicode equivalents of the aformentioned ANSI + functions. +*/ + static int -win32_stat(const char* path, struct win32_stat *result) +win32_lstat(const char* path, struct win32_stat *result) { WIN32_FILE_ATTRIBUTE_DATA info; int code; char *dot; + WIN32_FIND_DATAA find_data; + HANDLE find_data_handle; if (!GetFileAttributesExA(path, GetFileExInfoStandard, &info)) { if (GetLastError() != ERROR_SHARING_VIOLATION) { /* Protocol violation: we explicitly clear errno, instead of @@ -1049,6 +1068,25 @@ code = attribute_data_to_stat(&info, result); if (code != 0) return code; + + /* Get WIN32_FIND_DATA structure for the path to determine if + it is a symlink */ + if(info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + find_data_handle = FindFirstFileA(path, &find_data); + if(find_data_handle != INVALID_HANDLE_VALUE) + { + if(find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK) + { + /* first clear the S_IFMT bits */ + result->st_mode ^= (result->st_mode & 0170000); + /* now set the bits that make this a symlink */ + result->st_mode |= 0120000; + } + FindClose(find_data_handle); + } + } + /* Set S_IFEXEC if it is an .exe, .bat, ... */ dot = strrchr(path, '.'); if (dot) { @@ -1062,11 +1100,13 @@ } static int -win32_wstat(const wchar_t* path, struct win32_stat *result) +win32_lstat_w(const wchar_t* path, struct win32_stat *result) { int code; const wchar_t *dot; WIN32_FILE_ATTRIBUTE_DATA info; + WIN32_FIND_DATAW find_data; + HANDLE find_data_handle; if (!GetFileAttributesExW(path, GetFileExInfoStandard, &info)) { if (GetLastError() != ERROR_SHARING_VIOLATION) { /* Protocol violation: we explicitly clear errno, instead of @@ -1086,6 +1126,25 @@ code = attribute_data_to_stat(&info, result); if (code < 0) return code; + + /* Get WIN32_FIND_DATA structure for the path to determine if + it is a symlink */ + if(info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + find_data_handle = FindFirstFileW(path, &find_data); + if(find_data_handle != INVALID_HANDLE_VALUE) + { + if(find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK) + { + /* first clear the S_IFMT bits */ + result->st_mode ^= (result->st_mode & 0170000); + /* now set the bits that make this a symlink */ + result->st_mode |= 0120000; + } + FindClose(find_data_handle); + } + } + /* Set IFEXEC if it is an .exe, .bat, ... */ dot = wcsrchr(path, '.'); if (dot) { @@ -1098,7 +1157,128 @@ return code; } +/* Grab GetFinalPathNamyByHandle dynamically from kernel32 */ +static int has_GetFinalPathNameByHandle = 0; +static DWORD (CALLBACK *Py_GetFinalPathNameByHandleA)(HANDLE, LPSTR, DWORD, DWORD); +static DWORD (CALLBACK *Py_GetFinalPathNameByHandleW)(HANDLE, LPWSTR, DWORD, DWORD); static int +check_GetFinalPathNameByHandle() +{ + HINSTANCE hKernel32; + /* only recheck */ + if (!has_GetFinalPathNameByHandle) + { + hKernel32 = GetModuleHandle("KERNEL32"); + *(FARPROC*)&Py_GetFinalPathNameByHandleA = GetProcAddress(hKernel32, "GetFinalPathNameByHandleA"); + *(FARPROC*)&Py_GetFinalPathNameByHandleW = GetProcAddress(hKernel32, "GetFinalPathNameByHandleW"); + has_GetFinalPathNameByHandle = Py_GetFinalPathNameByHandleA && Py_GetFinalPathNameByHandleW; + } + return has_GetFinalPathNameByHandle; +} + +static int +win32_stat(const char* path, struct win32_stat *result) +{ + /* Traverse the symlink to the target using + GetFinalPathNameByHandle() + */ + int code; + HANDLE hFile; + int buf_size; + char *target_path; + int result_length; + + if(!check_GetFinalPathNameByHandle()) + { + return win32_lstat(path, result); + } + + hFile = CreateFileA( + path, + 0, /* desired access */ + 0, /* share mode */ + NULL, /* security attributes */ + OPEN_EXISTING, + /* FILE_FLAG_BACKUP_SEMANTICS is required to open a directory */ + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if(hFile == INVALID_HANDLE_VALUE) + { + // CreateFile will fail if the symlink points to a locked file + // such as pagefile.sys. For optimum correctness, this should + // instead attempt to resolve the symlink. For temporary + // backward compatibility, assume path is not a symlink and + // use lstat. + return win32_lstat(path, result); + } + + buf_size = Py_GetFinalPathNameByHandleA(hFile, 0, 0, VOLUME_NAME_DOS); + if(!buf_size) return -1; + target_path = (char *)malloc((buf_size+1)*sizeof(char)); + result_length = Py_GetFinalPathNameByHandleA(hFile, target_path, buf_size, VOLUME_NAME_DOS); + + if(!result_length) return -1; + if(!CloseHandle(hFile)) return -1; + target_path[result_length] = 0; + code = win32_lstat(target_path, result); + free(target_path); + + return code; +} + +static int +win32_stat_w(const wchar_t* path, struct win32_stat *result) +{ + /* Traverse the symlink to the target using + GetFinalPathNameByHandle() + */ + int code; + HANDLE hFile; + int buf_size; + wchar_t *target_path; + int result_length; + + if(!check_GetFinalPathNameByHandle()) + { + return win32_lstat_w(path, result); + } + + hFile = CreateFileW( + path, + 0, /* desired access */ + 0, /* share mode */ + NULL, /* security attributes */ + OPEN_EXISTING, + /* FILE_FLAG_BACKUP_SEMANTICS is required to open a directory */ + FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if(hFile == INVALID_HANDLE_VALUE) + { + // CreateFile will fail if the symlink points to a locked file + // such as pagefile.sys. For optimum correctness, this should + // instead attempt to resolve the symlink. For temporary + // backward compatibility, assume path is not a symlink and + // use lstat. + return win32_lstat_w(path, result); + } + + buf_size = Py_GetFinalPathNameByHandleW(hFile, 0, 0, VOLUME_NAME_DOS); + if(!buf_size) return -1; + target_path = (wchar_t *)malloc((buf_size+1)*sizeof(wchar_t)); + result_length = Py_GetFinalPathNameByHandleW(hFile, target_path, buf_size, VOLUME_NAME_DOS); + + if(!result_length) return -1; + if(!CloseHandle(hFile)) return -1; + target_path[result_length] = 0; + code = win32_lstat_w(target_path, result); + free(target_path); + + return code; +} + +static int win32_fstat(int file_number, struct win32_stat *result) { BY_HANDLE_FILE_INFORMATION info; @@ -2684,7 +2864,7 @@ posix_stat(PyObject *self, PyObject *args) { #ifdef MS_WINDOWS - return posix_do_stat(self, args, "O&:stat", STAT, "U:stat", win32_wstat); + return posix_do_stat(self, args, "O&:stat", STAT, "U:stat", win32_stat_w); #else return posix_do_stat(self, args, "O&:stat", STAT, NULL, NULL); #endif @@ -2737,7 +2917,44 @@ return PyLong_FromLong((long)i); } +#ifdef MS_WINDOWS +/* override the default DeleteFileW behavior so that directory +symlinks can be removed with this function, the same as with +Unix symlinks */ +BOOL WINAPI Py_DeleteFileW(LPCWSTR lpFileName) +{ + WIN32_FILE_ATTRIBUTE_DATA info; + WIN32_FIND_DATAW find_data; + HANDLE find_data_handle; + int is_directory = 0; + int is_link = 0; + + if (GetFileAttributesExW(lpFileName, GetFileExInfoStandard, &info)) { + is_directory = info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; + + /* Get WIN32_FIND_DATA structure for the path to determine if + it is a symlink */ + if(is_directory && + info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + find_data_handle = FindFirstFileW(lpFileName, &find_data); + if(find_data_handle != INVALID_HANDLE_VALUE) + { + is_link = find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK; + FindClose(find_data_handle); + } + } + } + + if (is_directory && is_link) + { + return RemoveDirectoryW(lpFileName); + } + return DeleteFileW(lpFileName); +} +#endif /* MS_WINDOWS */ + PyDoc_STRVAR(posix_unlink__doc__, "unlink(path)\n\n\ Remove a file (same as remove(path))."); @@ -2750,7 +2967,7 @@ posix_unlink(PyObject *self, PyObject *args) { #ifdef MS_WINDOWS - return win32_1str(args, "remove", "y:remove", DeleteFileA, "U:remove", DeleteFileW); + return win32_1str(args, "remove", "y:remove", DeleteFileA, "U:remove", Py_DeleteFileW); #else return posix_1str(args, "O&:remove", unlink); #endif @@ -4576,7 +4793,7 @@ return posix_do_stat(self, args, "O&:lstat", lstat, NULL, NULL); #else /* !HAVE_LSTAT */ #ifdef MS_WINDOWS - return posix_do_stat(self, args, "O&:lstat", STAT, "U:lstat", win32_wstat); + return posix_do_stat(self, args, "O&:lstat", STAT, "U:lstat", win32_lstat_w); #else return posix_do_stat(self, args, "O&:lstat", STAT, NULL, NULL); #endif @@ -4653,7 +4870,192 @@ } #endif /* HAVE_SYMLINK */ +#if !defined(HAVE_READLINK) && defined(MS_WINDOWS) +PyDoc_STRVAR(win_readlink__doc__, +"readlink(path) -> path\n\n\ +Return a string representing the path to which the symbolic link points."); + +// The following structure was copied from http://msdn.microsoft.com/en-us/library/ms791514.aspx +// as the required include doesn't seem to be present in the Windows SDK (at least as included +// with Visual Studio Express). +//#include "ntifs.h" +typedef struct _REPARSE_DATA_BUFFER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + union { + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } SymbolicLinkReparseBuffer; + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; + } MountPointReparseBuffer; + struct { + UCHAR DataBuffer[1]; + } GenericReparseBuffer; + }; +} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER; + +#define REPARSE_DATA_BUFFER_HEADER_SIZE FIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer) + +#define MAXIMUM_REPARSE_DATA_BUFFER_SIZE ( 16 * 1024 ) + +// Windows readlink implementation +static PyObject * +win_readlink(PyObject *self, PyObject *args) +{ + wchar_t *path; + DWORD n_bytes_returned; + DWORD io_result; + PyObject *result; + HANDLE reparse_point_handle; + + char target_buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + REPARSE_DATA_BUFFER *rdb = (REPARSE_DATA_BUFFER *)target_buffer; + wchar_t *print_name; + + if (!PyArg_ParseTuple(args, + "u:readlink", + &path)) + return NULL; + + // First get a handle to the reparse point + Py_BEGIN_ALLOW_THREADS + reparse_point_handle = CreateFileW( + path, + 0, + 0, + 0, + OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_BACKUP_SEMANTICS, + 0); + Py_END_ALLOW_THREADS + + if (reparse_point_handle==INVALID_HANDLE_VALUE) + { + return win32_error_unicode("readlink", path); + } + + Py_BEGIN_ALLOW_THREADS + // New call DeviceIoControl to read the reparse point + io_result = DeviceIoControl( + reparse_point_handle, + FSCTL_GET_REPARSE_POINT, + 0, 0, // in buffer + target_buffer, sizeof(target_buffer), + &n_bytes_returned, + 0 // we're not using OVERLAPPED_IO + ); + CloseHandle(reparse_point_handle); + Py_END_ALLOW_THREADS + + if (io_result==0) + { + return win32_error_unicode("readlink", path); + } + + if (rdb->ReparseTag != IO_REPARSE_TAG_SYMLINK) + { + PyErr_SetString(PyExc_ValueError, + "not a symbolic link"); + return NULL; + } + print_name = rdb->SymbolicLinkReparseBuffer.PathBuffer + rdb->SymbolicLinkReparseBuffer.PrintNameOffset; + result = PyUnicode_FromWideChar(print_name, rdb->SymbolicLinkReparseBuffer.PrintNameLength/2); + return result; +} + +#endif /* !defined(HAVE_READLINK) && defined(MS_WINDOWS) */ + +#if !defined(HAVE_SYMLINK) && defined(MS_WINDOWS) + +/* Grab CreateSymbolicLinkW dynamically from kernel32 */ +static int has_CreateSymbolicLinkW = 0; +static DWORD (CALLBACK *Py_CreateSymbolicLinkW)(LPWSTR, LPWSTR, DWORD); +static int +check_CreateSymbolicLinkW() +{ + HINSTANCE hKernel32; + /* only recheck */ + if (has_CreateSymbolicLinkW) + return has_CreateSymbolicLinkW; + hKernel32 = GetModuleHandle("KERNEL32"); + *(FARPROC*)&Py_CreateSymbolicLinkW = GetProcAddress(hKernel32, "CreateSymbolicLinkW"); + if (Py_CreateSymbolicLinkW) + has_CreateSymbolicLinkW = 1; + return has_CreateSymbolicLinkW; +} + +PyDoc_STRVAR(win_symlink__doc__, +"symlink(src, dst, target_is_directory=False)\n\n\ +Create a symbolic link pointing to src named dst.\n\n\ +target_is_directory is required if the target is to be interpreted as\n\n\ +a directory.\\\ +This function requires Windows 6.0 or greater, and raises a\n\n\ +NotImplementedError otherwise."); + +static PyObject * +win_symlink(PyObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"src", "dest", "target_is_directory", NULL}; + PyObject *src, *dest; + int target_is_directory = 0; + DWORD res; + WIN32_FILE_ATTRIBUTE_DATA src_info; + + if (!check_CreateSymbolicLinkW()) + { + /* raise NotImplementedError */ + return PyErr_Format(PyExc_NotImplementedError, + "CreateSymbolicLinkW not found"); + } + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|i:symlink", + kwlist, &src, &dest, &target_is_directory)) + return NULL; + if (!convert_to_unicode(&src)) { return NULL; } + if (!convert_to_unicode(&dest)) { + Py_DECREF(src); + return NULL; + } + + /* if src is a directory, ensure target_is_directory==1 */ + if( + GetFileAttributesExW( + PyUnicode_AsUnicode(src), GetFileExInfoStandard, &src_info + )) + { + target_is_directory = target_is_directory || + (src_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY); + } + + Py_BEGIN_ALLOW_THREADS + res = Py_CreateSymbolicLinkW( + PyUnicode_AsUnicode(dest), + PyUnicode_AsUnicode(src), + target_is_directory); + Py_END_ALLOW_THREADS + Py_DECREF(src); + Py_DECREF(dest); + if (!res) + { + return win32_error_unicode("symlink", PyUnicode_AsUnicode(src)); + } + + Py_INCREF(Py_None); + return Py_None; +} +#endif /* !defined(HAVE_SYMLINK) && defined(MS_WINDOWS) */ + #ifdef HAVE_TIMES #if defined(PYCC_VACPP) && defined(PYOS_OS2) static long @@ -7005,6 +7407,9 @@ #ifdef HAVE_READLINK {"readlink", posix_readlink, METH_VARARGS, posix_readlink__doc__}, #endif /* HAVE_READLINK */ +#if !defined(HAVE_READLINK) && defined(MS_WINDOWS) + {"readlink", win_readlink, METH_VARARGS, win_readlink__doc__}, +#endif /* !defined(HAVE_READLINK) && defined(MS_WINDOWS) */ {"rename", posix_rename, METH_VARARGS, posix_rename__doc__}, {"rmdir", posix_rmdir, METH_VARARGS, posix_rmdir__doc__}, {"stat", posix_stat, METH_VARARGS, posix_stat__doc__}, @@ -7012,6 +7417,9 @@ #ifdef HAVE_SYMLINK {"symlink", posix_symlink, METH_VARARGS, posix_symlink__doc__}, #endif /* HAVE_SYMLINK */ +#if !defined(HAVE_SYMLINK) && defined(MS_WINDOWS) + {"symlink", (PyCFunction)win_symlink, METH_VARARGS | METH_KEYWORDS, win_symlink__doc__}, +#endif /* !defined(HAVE_SYMLINK) && defined(MS_WINDOWS) #ifdef HAVE_SYSTEM {"system", posix_system, METH_VARARGS, posix_system__doc__}, #endif